/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jasper.compiler;

import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.UnsupportedEncodingException;
import java.util.ListIterator;
import javax.servlet.jsp.tagext.PageData;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.jasper.JasperException;

/**
 * An implementation of <tt>javax.servlet.jsp.tagext.PageData</tt> which builds
 * the XML view of a given page.
 * 
 * The XML view is built in two passes:
 * 
 * During the first pass, the FirstPassVisitor collects the attributes of the
 * top-level jsp:root and those of the jsp:root elements of any included pages,
 * and adds them to the jsp:root element of the XML view. In addition, any
 * taglib directives are converted into xmlns: attributes and added to the
 * jsp:root element of the XML view. This pass ignores any nodes other than
 * JspRoot and TaglibDirective.
 * 
 * During the second pass, the SecondPassVisitor produces the XML view, using
 * the combined jsp:root attributes determined in the first pass and any
 * remaining pages nodes (this pass ignores any JspRoot and TaglibDirective
 * nodes).
 * 
 * @author Jan Luehe
 */
class PageDataImpl extends PageData implements TagConstants {

	private static final String JSP_VERSION = "2.0";
	private static final String CDATA_START_SECTION = "<![CDATA[\n";
	private static final String CDATA_END_SECTION = "]]>\n";

	// string buffer used to build XML view
	private StringBuffer buf;

	/**
	 * Constructor.
	 * 
	 * @param page
	 *            the page nodes from which to generate the XML view
	 */
	public PageDataImpl(Node.Nodes page, Compiler compiler)
			throws JasperException {

		// First pass
		FirstPassVisitor firstPass = new FirstPassVisitor(page.getRoot(),
				compiler.getPageInfo());
		page.visit(firstPass);

		// Second pass
		buf = new StringBuffer();
		SecondPassVisitor secondPass = new SecondPassVisitor(page.getRoot(),
				buf, compiler, firstPass.getJspIdPrefix());
		page.visit(secondPass);
	}

	/**
	 * Returns the input stream of the XML view.
	 * 
	 * @return the input stream of the XML view
	 */
	public InputStream getInputStream() {
		// Turn StringBuffer into InputStream
		try {
			return new ByteArrayInputStream(buf.toString().getBytes("UTF-8"));
		} catch (UnsupportedEncodingException uee) {
			// should never happen
			throw new RuntimeException(uee.toString());
		}
	}

	/*
	 * First-pass Visitor for JspRoot nodes (representing jsp:root elements) and
	 * TablibDirective nodes, ignoring any other nodes.
	 * 
	 * The purpose of this Visitor is to collect the attributes of the top-level
	 * jsp:root and those of the jsp:root elements of any included pages, and
	 * add them to the jsp:root element of the XML view. In addition, this
	 * Visitor converts any taglib directives into xmlns: attributes and adds
	 * them to the jsp:root element of the XML view.
	 */
	static class FirstPassVisitor extends Node.Visitor implements TagConstants {

		private Node.Root root;
		private AttributesImpl rootAttrs;
		private PageInfo pageInfo;

		// Prefix for the 'id' attribute
		private String jspIdPrefix;

		/*
		 * Constructor
		 */
		public FirstPassVisitor(Node.Root root, PageInfo pageInfo) {
			this.root = root;
			this.pageInfo = pageInfo;
			this.rootAttrs = new AttributesImpl();
			this.rootAttrs
					.addAttribute("", "", "version", "CDATA", JSP_VERSION);
			this.jspIdPrefix = "jsp";
		}

		public void visit(Node.Root n) throws JasperException {
			visitBody(n);
			if (n == root) {
				/*
				 * Top-level page.
				 * 
				 * Add xmlns:jsp="http://java.sun.com/JSP/Page" attribute only
				 * if not already present.
				 */
				if (!JSP_URI.equals(rootAttrs.getValue("xmlns:jsp"))) {
					rootAttrs.addAttribute("", "", "xmlns:jsp", "CDATA",
							JSP_URI);
				}

				if (pageInfo.isJspPrefixHijacked()) {
					/*
					 * 'jsp' prefix has been hijacked, that is, bound to a
					 * namespace other than the JSP namespace. This means that
					 * when adding an 'id' attribute to each element, we can't
					 * use the 'jsp' prefix. Therefore, create a new prefix (one
					 * that is unique across the translation unit) for use by
					 * the 'id' attribute, and bind it to the JSP namespace
					 */
					jspIdPrefix += "jsp";
					while (pageInfo.containsPrefix(jspIdPrefix)) {
						jspIdPrefix += "jsp";
					}
					rootAttrs.addAttribute("", "", "xmlns:" + jspIdPrefix,
							"CDATA", JSP_URI);
				}

				root.setAttributes(rootAttrs);
			}
		}

		public void visit(Node.JspRoot n) throws JasperException {
			addAttributes(n.getTaglibAttributes());
			addAttributes(n.getNonTaglibXmlnsAttributes());
			addAttributes(n.getAttributes());

			visitBody(n);
		}

		/*
		 * Converts taglib directive into "xmlns:..." attribute of jsp:root
		 * element.
		 */
		public void visit(Node.TaglibDirective n) throws JasperException {
			Attributes attrs = n.getAttributes();
			if (attrs != null) {
				String qName = "xmlns:" + attrs.getValue("prefix");
				/*
				 * According to javadocs of org.xml.sax.helpers.AttributesImpl,
				 * the addAttribute method does not check to see if the
				 * specified attribute is already contained in the list: This is
				 * the application's responsibility!
				 */
				if (rootAttrs.getIndex(qName) == -1) {
					String location = attrs.getValue("uri");
					if (location != null) {
						if (location.startsWith("/")) {
							location = URN_JSPTLD + location;
						}
						rootAttrs
								.addAttribute("", "", qName, "CDATA", location);
					} else {
						location = attrs.getValue("tagdir");
						rootAttrs.addAttribute("", "", qName, "CDATA",
								URN_JSPTAGDIR + location);
					}
				}
			}
		}

		public String getJspIdPrefix() {
			return jspIdPrefix;
		}

		private void addAttributes(Attributes attrs) {
			if (attrs != null) {
				int len = attrs.getLength();

				for (int i = 0; i < len; i++) {
					String qName = attrs.getQName(i);
					if ("version".equals(qName)) {
						continue;
					}

					// Bugzilla 35252:
					// http://issues.apache.org/bugzilla/show_bug.cgi?id=35252
					if (rootAttrs.getIndex(qName) == -1) {
						rootAttrs.addAttribute(attrs.getURI(i), attrs
								.getLocalName(i), qName, attrs.getType(i),
								attrs.getValue(i));
					}
				}
			}
		}
	}

	/*
	 * Second-pass Visitor responsible for producing XML view and assigning each
	 * element a unique jsp:id attribute.
	 */
	static class SecondPassVisitor extends Node.Visitor implements TagConstants {

		private Node.Root root;
		private StringBuffer buf;
		private Compiler compiler;
		private String jspIdPrefix;
		private boolean resetDefaultNS = false;

		// Current value of jsp:id attribute
		private int jspId;

		/*
		 * Constructor
		 */
		public SecondPassVisitor(Node.Root root, StringBuffer buf,
				Compiler compiler, String jspIdPrefix) {
			this.root = root;
			this.buf = buf;
			this.compiler = compiler;
			this.jspIdPrefix = jspIdPrefix;
		}

		/*
		 * Visits root node.
		 */
		public void visit(Node.Root n) throws JasperException {
			if (n == this.root) {
				// top-level page
				appendXmlProlog();
				appendTag(n);
			} else {
				boolean resetDefaultNSSave = resetDefaultNS;
				if (n.isXmlSyntax()) {
					resetDefaultNS = true;
				}
				visitBody(n);
				resetDefaultNS = resetDefaultNSSave;
			}
		}

		/*
		 * Visits jsp:root element of JSP page in XML syntax.
		 * 
		 * Any nested jsp:root elements (from pages included via an include
		 * directive) are ignored.
		 */
		public void visit(Node.JspRoot n) throws JasperException {
			visitBody(n);
		}

		public void visit(Node.PageDirective n) throws JasperException {
			appendPageDirective(n);
		}

		public void visit(Node.IncludeDirective n) throws JasperException {
			// expand in place
			visitBody(n);
		}

		public void visit(Node.Comment n) throws JasperException {
			// Comments are ignored in XML view
		}

		public void visit(Node.Declaration n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.Expression n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.Scriptlet n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.JspElement n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.ELExpression n) throws JasperException {
			if (!n.getRoot().isXmlSyntax()) {
				buf.append("<").append(JSP_TEXT_ACTION);
				buf.append(" ");
				buf.append(jspIdPrefix);
				buf.append(":id=\"");
				buf.append(jspId++).append("\">");
			}
			buf.append("${");
			buf.append(JspUtil.escapeXml(n.getText()));
			buf.append("}");
			if (!n.getRoot().isXmlSyntax()) {
				buf.append(JSP_TEXT_ACTION_END);
			}
			buf.append("\n");
		}

		public void visit(Node.IncludeAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.ForwardAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.GetProperty n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.SetProperty n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.ParamAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.ParamsAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.FallBackAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.UseBean n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.PlugIn n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.NamedAttribute n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.JspBody n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.CustomTag n) throws JasperException {
			boolean resetDefaultNSSave = resetDefaultNS;
			appendTag(n, resetDefaultNS);
			resetDefaultNS = resetDefaultNSSave;
		}

		public void visit(Node.UninterpretedTag n) throws JasperException {
			boolean resetDefaultNSSave = resetDefaultNS;
			appendTag(n, resetDefaultNS);
			resetDefaultNS = resetDefaultNSSave;
		}

		public void visit(Node.JspText n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.DoBodyAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.InvokeAction n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.TagDirective n) throws JasperException {
			appendTagDirective(n);
		}

		public void visit(Node.AttributeDirective n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.VariableDirective n) throws JasperException {
			appendTag(n);
		}

		public void visit(Node.TemplateText n) throws JasperException {
			/*
			 * If the template text came from a JSP page written in JSP syntax,
			 * create a jsp:text element for it (JSP 5.3.2).
			 */
			appendText(n.getText(), !n.getRoot().isXmlSyntax());
		}

		/*
		 * Appends the given tag, including its body, to the XML view.
		 */
		private void appendTag(Node n) throws JasperException {
			appendTag(n, false);
		}

		/*
		 * Appends the given tag, including its body, to the XML view, and
		 * optionally reset default namespace to "", if none specified.
		 */
		private void appendTag(Node n, boolean addDefaultNS)
				throws JasperException {

			Node.Nodes body = n.getBody();
			String text = n.getText();

			buf.append("<").append(n.getQName());
			buf.append("\n");

			printAttributes(n, addDefaultNS);
			buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
			buf.append(jspId++).append("\"\n");

			if (ROOT_ACTION.equals(n.getLocalName()) || body != null
					|| text != null) {
				buf.append(">\n");
				if (ROOT_ACTION.equals(n.getLocalName())) {
					if (compiler.getCompilationContext().isTagFile()) {
						appendTagDirective();
					} else {
						appendPageDirective();
					}
				}
				if (body != null) {
					body.visit(this);
				} else {
					appendText(text, false);
				}
				buf.append("</" + n.getQName() + ">\n");
			} else {
				buf.append("/>\n");
			}
		}

		/*
		 * Appends the page directive with the given attributes to the XML view.
		 * 
		 * Since the import attribute of the page directive is the only page
		 * attribute that is allowed to appear multiple times within the same
		 * document, and since XML allows only single-value attributes, the
		 * values of multiple import attributes must be combined into one,
		 * separated by comma.
		 * 
		 * If the given page directive contains just 'contentType' and/or
		 * 'pageEncoding' attributes, we ignore it, as we've already appended a
		 * page directive containing just these two attributes.
		 */
		private void appendPageDirective(Node.PageDirective n) {
			boolean append = false;
			Attributes attrs = n.getAttributes();
			int len = (attrs == null) ? 0 : attrs.getLength();
			for (int i = 0; i < len; i++) {
				String attrName = attrs.getQName(i);
				if (!"pageEncoding".equals(attrName)
						&& !"contentType".equals(attrName)) {
					append = true;
					break;
				}
			}
			if (!append) {
				return;
			}

			buf.append("<").append(n.getQName());
			buf.append("\n");

			// append jsp:id
			buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
			buf.append(jspId++).append("\"\n");

			// append remaining attributes
			for (int i = 0; i < len; i++) {
				String attrName = attrs.getQName(i);
				if ("import".equals(attrName) || "contentType".equals(attrName)
						|| "pageEncoding".equals(attrName)) {
					/*
					 * Page directive's 'import' attribute is considered further
					 * down, and its 'pageEncoding' and 'contentType' attributes
					 * are ignored, since we've already appended a new page
					 * directive containing just these two attributes
					 */
					continue;
				}
				String value = attrs.getValue(i);
				buf.append("  ").append(attrName).append("=\"");
				buf.append(JspUtil.getExprInXml(value)).append("\"\n");
			}
			if (n.getImports().size() > 0) {
				// Concatenate names of imported classes/packages
				boolean first = true;
				ListIterator iter = n.getImports().listIterator();
				while (iter.hasNext()) {
					if (first) {
						first = false;
						buf.append("  import=\"");
					} else {
						buf.append(",");
					}
					buf.append(JspUtil.getExprInXml((String) iter.next()));
				}
				buf.append("\"\n");
			}
			buf.append("/>\n");
		}

		/*
		 * Appends a page directive with 'pageEncoding' and 'contentType'
		 * attributes.
		 * 
		 * The value of the 'pageEncoding' attribute is hard-coded to UTF-8,
		 * whereas the value of the 'contentType' attribute, which is identical
		 * to what the container will pass to ServletResponse.setContentType(),
		 * is derived from the pageInfo.
		 */
		private void appendPageDirective() {
			buf.append("<").append(JSP_PAGE_DIRECTIVE_ACTION);
			buf.append("\n");

			// append jsp:id
			buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
			buf.append(jspId++).append("\"\n");
			buf.append("  ").append("pageEncoding").append("=\"UTF-8\"\n");
			buf.append("  ").append("contentType").append("=\"");
			buf.append(compiler.getPageInfo().getContentType()).append("\"\n");
			buf.append("/>\n");
		}

		/*
		 * Appends the tag directive with the given attributes to the XML view.
		 * 
		 * If the given tag directive contains just a 'pageEncoding' attributes,
		 * we ignore it, as we've already appended a tag directive containing
		 * just this attributes.
		 */
		private void appendTagDirective(Node.TagDirective n)
				throws JasperException {

			boolean append = false;
			Attributes attrs = n.getAttributes();
			int len = (attrs == null) ? 0 : attrs.getLength();
			for (int i = 0; i < len; i++) {
				String attrName = attrs.getQName(i);
				if (!"pageEncoding".equals(attrName)) {
					append = true;
					break;
				}
			}
			if (!append) {
				return;
			}

			appendTag(n);
		}

		/*
		 * Appends a tag directive containing a single 'pageEncoding' attribute
		 * whose value is hard-coded to UTF-8.
		 */
		private void appendTagDirective() {
			buf.append("<").append(JSP_TAG_DIRECTIVE_ACTION);
			buf.append("\n");

			// append jsp:id
			buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
			buf.append(jspId++).append("\"\n");
			buf.append("  ").append("pageEncoding").append("=\"UTF-8\"\n");
			buf.append("/>\n");
		}

		private void appendText(String text, boolean createJspTextElement) {
			if (createJspTextElement) {
				buf.append("<").append(JSP_TEXT_ACTION);
				buf.append("\n");

				// append jsp:id
				buf.append("  ").append(jspIdPrefix).append(":id")
						.append("=\"");
				buf.append(jspId++).append("\"\n");
				buf.append(">\n");

				appendCDATA(text);
				buf.append(JSP_TEXT_ACTION_END);
				buf.append("\n");
			} else {
				appendCDATA(text);
			}
		}

		/*
		 * Appends the given text as a CDATA section to the XML view, unless the
		 * text has already been marked as CDATA.
		 */
		private void appendCDATA(String text) {
			buf.append(CDATA_START_SECTION);
			buf.append(escapeCDATA(text));
			buf.append(CDATA_END_SECTION);
		}

		/*
		 * Escapes any occurrences of "]]>" (by replacing them with "]]&gt;")
		 * within the given text, so it can be included in a CDATA section.
		 */
		private String escapeCDATA(String text) {
			if (text == null)
				return "";
			int len = text.length();
			CharArrayWriter result = new CharArrayWriter(len);
			for (int i = 0; i < len; i++) {
				if (((i + 2) < len) && (text.charAt(i) == ']')
						&& (text.charAt(i + 1) == ']')
						&& (text.charAt(i + 2) == '>')) {
					// match found
					result.write(']');
					result.write(']');
					result.write('&');
					result.write('g');
					result.write('t');
					result.write(';');
					i += 2;
				} else {
					result.write(text.charAt(i));
				}
			}
			return result.toString();
		}

		/*
		 * Appends the attributes of the given Node to the XML view.
		 */
		private void printAttributes(Node n, boolean addDefaultNS) {

			/*
			 * Append "xmlns" attributes that represent tag libraries
			 */
			Attributes attrs = n.getTaglibAttributes();
			int len = (attrs == null) ? 0 : attrs.getLength();
			for (int i = 0; i < len; i++) {
				String name = attrs.getQName(i);
				String value = attrs.getValue(i);
				buf.append("  ").append(name).append("=\"").append(value)
						.append("\"\n");
			}

			/*
			 * Append "xmlns" attributes that do not represent tag libraries
			 */
			attrs = n.getNonTaglibXmlnsAttributes();
			len = (attrs == null) ? 0 : attrs.getLength();
			boolean defaultNSSeen = false;
			for (int i = 0; i < len; i++) {
				String name = attrs.getQName(i);
				String value = attrs.getValue(i);
				buf.append("  ").append(name).append("=\"").append(value)
						.append("\"\n");
				defaultNSSeen |= "xmlns".equals(name);
			}
			if (addDefaultNS && !defaultNSSeen) {
				buf.append("  xmlns=\"\"\n");
			}
			resetDefaultNS = false;

			/*
			 * Append all other attributes
			 */
			attrs = n.getAttributes();
			len = (attrs == null) ? 0 : attrs.getLength();
			for (int i = 0; i < len; i++) {
				String name = attrs.getQName(i);
				String value = attrs.getValue(i);
				buf.append("  ").append(name).append("=\"");
				buf.append(JspUtil.getExprInXml(value)).append("\"\n");
			}
		}

		/*
		 * Appends XML prolog with encoding declaration.
		 */
		private void appendXmlProlog() {
			buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
		}
	}
}
